
import config
from skimpyGimpy import bdf

class LayoutGrid:
    def __init__(self):
        self.IJtoBox = {}
        self.nameToBox = {}
        self.connections = {}
        self.m = self.n = 0
        self.horizontals = self.verticals = None
    def addBox(self, box, i,j):
        box.visible = True
        gridIndex = (i,j)
        name = box.name
        assert gridIndex not in self.IJtoBox, "duplicate index "+repr(gridIndex)
        assert name not in self.nameToBox, "duplicate box name "+repr(name)
        box.gridPosition(i,j)
        box.visible = True
        self.IJtoBox[ gridIndex ] = box
        self.nameToBox[name] = box
        self.m = max(self.m, i+1)
        self.n = max(self.n, j+1)
    def connect(self, fromBoxName, fromAttrName, toBoxName, mandatory=True, onto=False, oneToOne=False, isMultiple=False):
        fromBox = self.nameToBox[fromBoxName]
        toBox = self.nameToBox[toBoxName]
        (i,j) = (fromBox.i, fromBox.j)
        (k,l) = (toBox.i, toBox.j)
        self.connections[ (i,j,k,l,fromAttrName) ] = (fromBox, toBox, mandatory, onto, oneToOne, isMultiple)
    def draw(self, canvas, cfg):
        rowIndices = range(self.m)
        colIndices = range(self.n)
        IJtoBox = self.IJtoBox
        # add any invisible boxes as needed
        for i in rowIndices:
            for j in colIndices:
                coords = (i,j)
                if not coords in IJtoBox:
                    #pr "adding invisible box at", coords
                    b = RelationBox(None, canvas, cfg)
                    b.gridPosition(i,j)
                    b.visible = False
                    b.invisibleGeometry()
                    self.IJtoBox[coords] = b
        # create horizontals at top
        horizontals = self.horizontals = {}
        for i in rowIndices:
            for j in colIndices:
                horizontals[ (i,j) ] = HorizontalBar(i,j,canvas,cfg)
        # create verticals, one group for each column following the column
        verticals = self.verticals = {}
        for j in colIndices:
            for k in rowIndices:
                for l in colIndices:
                    verticals[ (j,k,l) ] = VerticalBar(j,k,l, canvas, cfg)
        # mark visibles
        for quad in self.connections:
            (i,j,k,l, fromAttribute) = quad
            (fromBox,toBox) = self.connections[quad][:2]
            h = horizontals[ (k,l) ]
            h.visible = True
            v = verticals[ (j,k,l) ]
            v.visible = True
            #pr  "anchoring", toBox.name
            toBox.anchored = True
        # compute geometry for visible boxes
        for boxName in self.nameToBox:
            box = self.nameToBox[boxName]
            if box.visible:
                box.geometry()
        # find max widths and heights
        widths = [0] * self.n
        heights = [0] * self.m
        for pair in self.IJtoBox:
            (row, column) = pair
            box = self.IJtoBox[pair]
            widths[column] = max(widths[column], box.offsetWidth(self))
            heights[row] = max(heights[row], box.height)
        self.heights = heights
        #pr "heights are", heights
        self.widths = widths
        # set layouts
        for boxName in self.nameToBox:
            box = self.nameToBox[boxName]
            if box.visible:
                box.layout(self)
        for (i,j,k,l,a) in self.connections:
            h = horizontals[ (k,l) ]
            h.layout(self)
            v = verticals[ (l,i,j) ]
            v.layout(self)
        self.setLineLimits()
        # delete invisible lines
        for i in rowIndices:
            for j in colIndices:
                ind = (i,j)
                h = horizontals[ind]
                if not h.visible:
                    del horizontals[ind]
        for j in colIndices:
            for k in rowIndices:
                for l in colIndices:
                    ind = (j,k,l)
                    v = verticals[ind]
                    if not v.visible:
                        del verticals[ind]
        # shift up non-intersecting horizontal bars
        shifty = 0
        for i in rowIndices:
            yToBar = {}
            for j in colIndices:
                ind = (i,j)
                h = horizontals.get(ind)
                if h:
                    newy = h.yposition(self) + shifty
                    yToBar[ newy ] = [h]
            rowshift = collapseBars(yToBar)
            shifty += rowshift
            if shifty!=0:
                #pr  "row", i, "shifted", shifty
                for y in yToBar:
                    for h in yToBar[y]:
                        #h.y = y + rowshift
                        pass
                # shift boxes up
                for j in colIndices:
                    box = self.IJtoBox[(i,j)]
                    if box and box.visible:
                        #box.shiftY(shifty)
                        pass
        # shift left non-intersecting vertical bars
        shiftx = 0
        self.setLineLimits()
        for j in colIndices:
            # shift boxes left
            for k in rowIndices:
                box = self.IJtoBox[(k,j)]
                if box and box.visible:
                    box.shiftX(shiftx)
            xToBar = {}
            # determine shifts for bars for this column
            for k in rowIndices:
                for l in colIndices:
                    ind = (j,k,l)
                    v = verticals.get(ind)
                    if v:
                        newx = v.xposition(self)+shiftx
                        xToBar[ newx ] = [v]
            shiftx -= collapseBars(xToBar)
            for x in xToBar:
                for v in xToBar[x]:
                    v.x = x
                    pass
        self.setLineLimits()
        # draw all visible components
        for (i,j,k,l,a) in self.connections:
            h = horizontals[ (k,l) ]
            h.draw()
            v = verticals[ (j,k,l) ]
            v.draw()
        for boxName in self.nameToBox:
            box = self.nameToBox[boxName]
            if box.visible:
                box.draw()
    def setLineLimits(self):
        for quad in self.connections:
            (i,j,k,l,fromAttrName) = quad
            (fromBox,toBox,mandatory, onto, oneToOne, multiple) = self.connections[quad]
            h = self.horizontals[ (k,l) ]
            v = self.verticals[ (j,k,l) ]
            h.reset()
            v.reset()
        # set line limits
        for quad in self.connections:
            (i,j,k,l,fromAttrName) = quad
            (fromBox,toBox,mandatory, onto, oneToOne, multiple) = self.connections[quad]
            h = self.horizontals[ (k,l) ]
            v = self.verticals[ (j,k,l) ]
            h.reachX( toBox.middlex() )
            #h.reachX( toBox.xposition(self) )
            h.reachX( v.xposition(self) )
            v.reachY( fromBox.nameY(fromAttrName) )
            v.reachY( h.yposition(self) )
            v.anchorY = h.yposition(self) 
            # need to add cardinality info xxxx
            fromBox.addLinkage(fromAttrName, v.xposition(self), mandatory, onto, oneToOne, multiple)
            toBox.anchorY = h.yposition(self)        

def collapseBars(offsetToBar):
    if not offsetToBar:
        return 0
    offsets = offsetToBar.keys()
    offsets.sort()
    initialDifference = max(offsets) - min(offsets)
    for fixOffset in offsets:
        fixBars = offsetToBar[fixOffset]
        for testOffset in offsets:
            if testOffset<fixOffset:
                testBars = offsetToBar[testOffset]
                # try to move each fixBar into test position
                for fixBar in fixBars[:]:
                    moveable = True
                    for testBar in testBars:
                        if fixBar.intersects(testBar):
                            #pr  "intersecting", fixBar, testBar
                            moveable = False
                    if moveable:
                        #pr  "MOVING", fixOffset, testOffset, len(fixBars), len(testBars)
                        fixBars.remove(fixBar)
                        testBars.append(fixBar)
            else:
                break # past fix position
    for offset in offsets:
        if not offsetToBar[offset]:
            #pr  "ELIMINATING OFFSET", offset
            del offsetToBar[offset]
    newoffsets = offsetToBar.keys()
    finalDifference = max(newoffsets) - min(newoffsets)
    saved =  initialDifference - finalDifference
    #pr  "SAVED", saved
    return saved

class LayoutComponent:
    visible = False
    x = y = None
    def xposition(self, layout):
        if self.x is None:
            self.layout(layout)
        return self.x
    def yposition(self, layout):
        if self.y is None:
            self.layout(layout)
        return self.y
    def layout(self, layout):
        raise ValueError, "implement at subclass"

def solder(c, x, y, d=5):
    c.addRect(x-d/2,y-d/2,d,d)

class VerticalBar(LayoutComponent):
    visible = False
    def __init__(self, j,k,l, canvas, cfg):
        (self.j, self.k, self.l, self.canvas, self.cfg) = (j,k,l,canvas,cfg)
        self.anchorY = self.x = self.starty = self.endy = None
    def reset(self):
        self.starty = self.endy = None
    def __repr__(self):
        return "vertical[%s](%s..%s)" % (self.x, self.starty, self.endy)
    def contains(self, y):
        return self.starty-4<=y and y<=self.endy+4
    def intersects(self, other):
        return self.contains(other.starty) or self.contains(other.endy) or other.contains(self.starty) or other.contains(self.endy)
    def layout(self, layout):
        if self.x is not None:
            return
        d = self.cfg.delta
        (j,k,l) = (self.j, self.k, self.l)
        if j==0 and k==0 and l==0:
            # left most in first column
            self.x = layout.widths[0]
        elif k==0 and l==0:
            # leftmost in non-first column
            #pr  "(j-1, layout.m-1, layout.n-1)", (j-1, layout.m-1, layout.n-1)
            #pr  "layout.verticals.keys()", layout.verticals.keys()
            barToLeft = layout.verticals[ (j-1, layout.m-1, layout.n-1) ]
            xbase = barToLeft.xposition(layout)
            self.x = xbase + d*2.7 + layout.widths[j]
        else:
            if l==0:
                # leftmost in group
                barToLeft = layout.verticals[ (j, k-1, layout.n-1) ]
            else:
                barToLeft = layout.verticals[ (j, k, l-1) ]
            xbase = barToLeft.xposition(layout)
            self.x = xbase
        if self.visible:
            self.x += d*2.22
        self.x = int(self.x)
    def reachY(self, y):
        (sy,ey) = (self.starty, self.endy)
        if sy is None:
            self.starty = self.endy = y
        else:
            self.starty = min(y, sy)
            self.endy = max(y, ey)
    def draw(self):
        if not self.visible:
            return
        c = self.canvas
        cfg = self.cfg
        c.setColor(0,0,100)
        solder(c, self.x, self.starty)
        solder(c, self.x, self.endy)
        solder(c, self.x, self.anchorY, 8)
        c.addLines( [ (self.x, self.starty), (self.x, self.endy) ] )

class HorizontalBar(LayoutComponent):
    visible = False
    def __init__(self, i, j, canvas, cfg):
        self.canvas = canvas
        self.cfg = cfg
        self.i = i
        self.j = j
        self.y = self.startx = self.endx = None
    def reset(self):
        self.startx = self.endx = None
    def __repr__(self):
        return "horizontal(%s..%s)[%s]" % (self.startx, self.endx, self.y)
    def contains(self, x):
        #pr "contains", [self.startx, x, self.endx], self.startx<=x and x<=self.endx
        return self.startx-4<=x and x<=self.endx+4
    def intersects(self, other):
        return self.contains(other.startx) or self.contains(other.endx) or other.contains(self.startx) or other.contains(self.endx)
    def layout(self, layout):
        if self.y is not None:
            return
        (i,j) = (self.i, self.j)
        d = self.cfg.delta
        if i==0 and j==0:
            # top bar
            self.y = 0
        elif j==0:
            # top of group for row
            above = layout.horizontals[(i-1, layout.n-1)]
            self.y = above.yposition(layout) - layout.heights[i-1] - 2*d
            #aboveBox = layout.IJtoBox[ (i-1,0) ]
            ##pr "horizontal abovebox at", aboveBox.yposition(layout)
            #self.y = aboveBox.yposition(layout) - 1.7*d
        else:
            above = layout.horizontals[(i, j-1)]
            self.y = above.yposition(layout)
        if self.visible:
            self.y -= d*1.5
        self.y = int(self.y)
    def reachX(self, x):
        (sx,ex) = (self.startx, self.endx)
        if sx is None:
            self.startx = self.endx = x
        else:
            self.startx = min(x, sx)
            self.endx = max(x, ex)
    def draw(self):
        if not self.visible:
            return
        c = self.canvas
        cfg = self.cfg
        c.setColor(0,100,0)
        solder(c, self.startx, self.y)
        solder(c, self.endx, self.y)
        c.addLines( [ (self.startx, self.y), (self.endx, self.y) ] )

class RelationBox(LayoutComponent):
    def __init__(self, name, canvas, cfg):
        if name:
            self.visible = True
        self.canvas = canvas
        self.cfg = cfg
        self.name = name
        self.x = self.y = None
        self.i = self.j = None
        self.offsetX = None
        self.nameXoffset = self.nameYoffset = None
        self.attributeXoffset = None
        self.height = self.width = None
        self.anchored = False
        self.anchorY = None
        self.attrList = []
        self.attrTypes = {}
        self.links = {}
        self.keys = {}
        self.NameToYoffset = {}
        self.NameToEndXoffset = {}
        self.linkToConnectX = {}
    def layout(self, layout):
        if self.y is not None:
            return
        (i,j) = (self.i, self.j)
        d = self.cfg.delta
        # x position
        if j==0:
            self.x = 0
        else:
            leftbar = layout.verticals[ (j-1, layout.m-1, layout.n-1) ]
            self.x = leftbar.xposition(layout) + d*2.33
        self.x = self.x+self.xOffset(layout)
        hi = layout.heights[i]
        wj = layout.widths[j]
        # left justify the box in the column
        self.x += wj-self.width
        upbar = layout.horizontals[ (i, layout.n-1) ]
        self.y = upbar.yposition(layout) - d - hi
        # align top the box in the row
        self.y += hi - self.height
    def gridPosition(self, i, j):
        self.i = i
        self.j = j
    def addLinkage(self, name, connectX, mandatory=True, onto=False, oneToOne=False, multiple=False):
        self.links[name] = (mandatory, onto, oneToOne, multiple)
        self.linkToConnectX[name] = connectX
    def addAttribute(self, name, isKey=False, isRequired=True):
        self.attrList.append(name)
        if isKey:
            self.keys[name] = True
    def position(self, x, y):
        (self.x, self.y) = (x,y)
    def middlex(self):
        return self.x + self.width/2
    def geometry(self):
        c = self.canvas
        cfg = self.cfg
        d = cfg.delta
        textHeight = cfg.fontHeight
        xmax = 0
        yoffset = -d-textHeight*1.2
        self.nameYoffset = yoffset
        yoffset -= d
        self.nameXoffset = d
        self.attributeXoffset = d*2
        width = self.textWidth(c, cfg, self.name) + d*2
        #pr "initial yoffset", (self.name, yoffset)
        for attr in self.attrList:
            yoffset -= textHeight*1.4
            #pr "yoffset adjusted for attribute", (self.name, yoffset)
            self.NameToYoffset[attr] = yoffset
            tw = self.textWidth(c, cfg, attr)
            width = max(width, tw+d*8)
            self.NameToEndXoffset[attr] = self.attributeXoffset + tw
        self.height = -yoffset + d
        self.width = width
        #pr "geometry", (self.name, self.width, self.height)
    def textWidth(self, c, cfg, text):
        return 7*len(text) # xxxx temporary hack
        #fontName = cfg.fontName
        #font = c.getFont(fontName)
        #pixels = bdf.pixelation(font, text)
        #return pixels.width()
    def nameY(self, name):
        ytop = self.y+self.height
        return ytop + self.NameToYoffset[name] + 4
    def invisibleGeometry(self):
        self.width = self.height = 0
    def offsetWidth(self, layout):
        return self.width+self.xOffset(layout)
    def xOffset(self, layout):
        if True:
            self.offsetX = 0
            return 0
        if self.offsetX is not None:
            return self.offsetX
        row = self.i
        column = self.j
        #pr  "row, column, layout.m, layout.n, layout.IJtoBox.keys()", row, column, layout.m, layout.n, layout.IJtoBox.keys()
        if row==layout.m-1:
            #pr  "bottom row"
            result = 0
        else:
            #pr  "not bottom row"
            boxBelow = layout.IJtoBox[ (row+1, column) ]
            result = boxBelow.xOffset(layout)
            if boxBelow.anchored:
                #pr  "boxBelow is anchored"
                result += self.cfg.delta
        self.offsetX = result
        #pr  "returning offsetX", result
        return result
    def shiftX(self, dx):
        self.x += dx
    def shiftY(self, dy):
        #pr "shiftY", self.name, self.y, dy, self.y+dy
        self.y += dy
    def draw(self):
        if not self.visible:
            return
        c = self.canvas
        cfg = self.cfg
        d = cfg.delta
        (x,y) = (self.x, self.y)
        #pr "drawing", self.name, "at", (x,y)
        ytop = y+self.height
        if self.anchored:
            assert self.anchorY is not None, "cannot draw if anchored set but anchor Y not specified"
            # make anchor line
            xm = self.middlex()
            c.setColor(50,50,0)
            solder(c, xm, ytop, 10)
            solder(c, xm, self.anchorY)
            c.addLines( [ (xm, ytop), (xm, self.anchorY) ])
        c.setColor(233,233,100)
        c.addRect(x, y, self.width, self.height, self.name)
        c.setFont(cfg.fontName, 1.0, 0.2)
        c.setColor(11,33,53)
        c.addRect(x, ytop+self.nameYoffset-2, self.width, 2-self.nameYoffset)
        c.setColor(155,200,250)
        c.addText(x+self.nameXoffset, ytop+self.nameYoffset, self.name)
        for name in self.attrList:
            ty = ytop + self.NameToYoffset[name]
            tx = x+self.attributeXoffset
            if name in self.keys:
                c.setColor(10,150,25)
                uEndX = x + self.NameToEndXoffset[name]
                uy = ty-4
                c.addLines([ (tx, uy), (uEndX, uy) ] )
            c.setColor(50,10,50)
            c.addText(tx, ty, name)
            connectX = self.linkToConnectX.get(name)
            if connectX is not None:
                connectStartX = x + self.NameToEndXoffset[name] + d/2
                connectY = self.nameY(name) #ytop + self.NameToYoffset[name] + 4
                c.setColor(10,80,30)
                solder(c, connectX, connectY)
                c.addLines([ (connectStartX, connectY), (connectX, connectY) ])
                (mandatory, onto, oneToOne, multiple) = self.links[name]
                if multiple:
                    c.addLines([ (connectX-d/2, connectY+d/2), 
                                 (connectX-d, connectY),
                                 (connectX-d/2, connectY-d/2) ])                    
                else:
                    c.addLines([ (connectX-d*0.7, connectY+d/2), (connectX-d*0.7, connectY-d/2) ])
                if mandatory:
                    #pr name, "mandatory"
                    c.addLines([ (connectX-d*1.5, connectY+d/2), (connectX-d*1.5, connectY-d/2) ])
                else:
                    #pr name, "optional"
                    c.addCircle( connectX-d*1.5, connectY, d/2 )
                if onto:
                    #pr name, "onto"
                    c.addLines([ (connectStartX+d*2, connectY+d/2), (connectStartX+d*2, connectY-d/2) ])
                else:
                    #pr name, "not onto"
                    c.addCircle( connectStartX+d*2, connectY, d/2 )
                if oneToOne:
                    #pr name, "oneToOne"
                    c.addLines([ (connectStartX+d, connectY+d/2), (connectStartX+d, connectY-d/2) ])
                else:
                    #pr name, "not oneToOne"
                    c.addLines([ (connectStartX+d/2, connectY+d/2), 
                                 (connectStartX+d, connectY),
                                 (connectStartX+d/2, connectY-d/2) ])
